Skip to content

Parquet v3 read concurrency - #2235

Merged
mkmkme merged 14 commits into
antalya-26.6from
parquet-v3-read-concurrency
Aug 25, 2026
Merged

Parquet v3 read concurrency#2235
mkmkme merged 14 commits into
antalya-26.6from
parquet-v3-read-concurrency

Conversation

@UnamedRus

Copy link
Copy Markdown
Collaborator

Number of changes to bring parquet v3 reader perf closer to arrow based

Changelog category (leave one):

  • Performance Improvement

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

Workflow [PR], commit [466d6cf]

Budget memory and threads separately per read stage instead of one shared
fraction, and add a ColumnDataPrefetch stage that issues the compressed
data-page reads (charged to its own memory budget) while ColumnData only
decodes. The old single 0.2 fraction capped the data stage at 0.2 of both
memory and threads, so only ~2 row groups were read/decoded ahead and the S3
link sat idle on latency-bound, high-RTT reads. Now compressed reads run deep
(cheap per row group) while decoded row groups stay bounded, hiding per-GET
latency. Also reconcile the decoded-memory charge up to the actual footprint
inside decodePrimitiveColumn, before formOutputColumn moves the column, so the
honest cap actually bounds decode-ahead.

Squashed extraction of 34816a3 + 114640e + f260506 from the
parquet-v3 feature branch onto antalya-26.6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: UnamedRus <dtitmoav@gmail.com>
@mkmkme

mkmkme commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Could you add Changelog entry please?

mkmkme
mkmkme previously approved these changes Aug 19, 2026

@mkmkme mkmkme left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good from the brief look

@UnamedRus

Copy link
Copy Markdown
Collaborator Author

Will do (and benchmark justification), but i would ask to start reviewing of it tomorrow, i want to do some small changes and remove some slop. :)

/// Budget memory and threads separately: a single 0.2 fraction capped ColumnData at 0.2 of both,
/// so only ~2 row groups were read/decoded ahead. Give ColumnData most of the memory and threads
/// (deep, decode-bound); give the small latency-bound index/bloom reads threads for parallelism.
using S = ReadStage;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Useless alias. Keeps several characters in each line, but adds cognitive complexity.

set_fractions(S::ColumnDataPrefetch, 0.45, 1);
/// Decode: bounded memory (decoded row groups are large) but most threads. Caps resident decoded
/// row groups independently of prefetch depth.
set_fractions(S::ColumnData, 0.30, 3);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each stage get part of memory/thread budget based on some magic numbers.
I trust that numbers has a ground, but good to add it somewhere.

UnamedRus and others added 2 commits August 21, 2026 16:11
The reader scheduler splits a static memory and thread budget across the
read stages (bloom/index reads, compressed prefetch, decode) using fixed
relative weights. Those weights are the main lever for latency-bound
object-storage reads - the compressed prefetch weight sets the read-ahead
depth, the decode thread weight sets decode concurrency - but they were
hardcoded, so tuning them required a rebuild.

Add `input_format_parquet_read_stage_weights`, an experimental `Map`
setting. Keys are `<stage>.<resource>` (resource is `memory` or `threads`);
values are relative weights, normalized per resource across stages exactly
like the built-in defaults. Only the listed keys override; unspecified
stages keep their defaults, so an empty map (the default) reproduces the
previous behavior bit for bit.

Unknown stage or resource names, negative weights, and a configuration that
zeroes out the whole memory or thread budget all raise `BAD_ARGUMENTS`
rather than silently falling back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: UnamedRus <dtitmoav@gmail.com>
…atches

`ColumnDataPrefetch` tasks do all their work (`determinePagesToPrefetch` +
`startPrefetch`) synchronously in `scheduleTask`; their `runTask` body is empty
and the reads proceed asynchronously in the Prefetcher's io pool. Their run time
is therefore ~0, but `cost_estimate_bytes` was set to the charged compressed
bytes, which are large.

`scheduleTasksIfNeeded` uses `cost_estimate_bytes` as a proxy for run time to
group tiny tasks into batches. Reporting the large compressed-byte charge made
the batcher split these zero-work tasks across up to `parsing_threads` batches -
one thread-pool dispatch each - for tasks that do nothing on the thread.

Report a cost of 0 for `ColumnDataPrefetch` tasks so they collapse into a single
batch, removing the spurious per-subgroup dispatch overhead introduced by the
prefetch/decode stage split.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@UnamedRus
UnamedRus force-pushed the parquet-v3-read-concurrency branch from ccd3c2d to df7105e Compare August 24, 2026 09:34
UnamedRus and others added 2 commits August 24, 2026 12:40
Condense the oversized comment blocks added by the read-concurrency work (the
stage-weight rationale in `ReadManager::init`, the ColumnDataPrefetch/ColumnData
scheduleTask and runTask notes, and the cost-estimate exception) to a few lines
each. No code changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/Formats/FormatFactory.cpp Outdated
format_settings.parquet.enable_json_parsing = settings[Setting::input_format_parquet_enable_json_parsing];
format_settings.parquet.memory_low_watermark = settings[Setting::input_format_parquet_memory_low_watermark];
format_settings.parquet.memory_high_watermark = settings[Setting::input_format_parquet_memory_high_watermark];
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parsing like this already exists for http_response_headers setting. Suggestion is to move login into template function and reuse for both settings.
https://github.com/Altinity/ClickHouse/blob/antalya-26.6/src/Interpreters/executeQuery.cpp#L2447

Comment thread src/Core/FormatFactorySettings.h Outdated
Approximate memory limit for Parquet reader v3. Limits how many row groups or columns can be read in parallel. When reading multiple files in one query, the limit is on total memory usage across those files.
)", 0) \
DECLARE(Map, input_format_parquet_read_stage_weights, "", R"(
Expert tuning knob for the Parquet reader scheduler. Overrides the relative memory and thread budget weights assigned to each read stage. Keys have the form `<stage>.<resource>`, where `<resource>` is `memory` or `threads` and `<stage>` is one of `bloom_filter_header`, `bloom_filter_blocks_or_dictionary`, `column_index_and_offset_index`, `offset_index`, `column_data_prefetch`, `column_data`. Values are relative weights (they need not sum to 1; each is normalized against the sum of that resource across stages). Only the specified keys override the built-in defaults; unspecified stages keep their defaults. Example: `{'column_data_prefetch.memory': 12, 'column_data.threads': 4}`. Advanced and experimental: the internal stage model may change between versions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unspecified stages keep their defaults

How user can know default values except reading source code? Now if I change some value, I even have no idea it increased or decreased.

And key-value pairs in string setting when keys must be in short predefined list is weird, not as all other settings in CH.
Only one sample like this is http_response_headers, but http_response_headers has unspecified list of possible keys.
12 different settings instead look just as good as this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to replace with 2 float setting, which actually encode 90% of useful stuff (ie between prefetch and decode)

input_format_parquet_prefetch_memory_fraction
input_format_parquet_decode_thread_fraction

UnamedRus and others added 7 commits August 25, 2026 11:32
…nobs

The `input_format_parquet_read_stage_weights` `Map` setting failed to build:
`Map` is not in `FORMAT_FACTORY_SETTINGS_SUPPORTED_TYPES` (nor in the supported
types of the collections that embed the format settings, e.g.
`DataLakeStorageSettings`), so the settings-layout machinery has no
`SettingTypeTag::Map` for those collections:

    DataLakeStorageSettings.cpp: error: no member named 'Map' in
      'DataLakeStorageSettingsTraitsDetail::SettingTypeTag'
      ...expanded from DECLARE(Map, input_format_parquet_read_stage_weights, ...)

Adding `Map` to every such collection's supported-types list is invasive and is
why no format setting is a `Map` today. Replace the single `Map` with two `Float`
settings (`Float` is already a supported format-settings type):

  - `input_format_parquet_prefetch_memory_fraction` (default 0.6): of the memory
    budget reserved for column data, the share given to compressed read-ahead
    (`ColumnDataPrefetch`) vs decoded output (`ColumnData`).
  - `input_format_parquet_decode_thread_fraction` (default 0.375): `ColumnData`'s
    share of the parsing thread pool; the read-issuing stages split the rest.

These cover the two binding constraints on decode-ahead depth (memory on
latency-bound remote reads, threads on CPU-bound local decode) without exposing
the dead per-stage knobs for the read-issuing stages. `ReadManager::init` now
computes per-stage fractions directly from the two knobs; the defaults reproduce
the previously hard-coded fractions exactly, so behavior is unchanged by default.
The index/bloom stages keep fixed shares.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The native reader is now the only one (input_format_parquet_use_native_reader_v3
is obsolete/always-on), so the "v3" qualifier in the user-facing setting
descriptions and code comments is stale. Identifiers and the obsolete-setting
history entries keep their names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two knobs were stored both in FormatSettings.parquet and copied into
SharedResourcesExt (mirroring the memory watermarks). The watermarks live in the
ext because getLimitsPerReader (a static method without options access) reads
them; the fractions are only consumed in ReadManager::init, which already has
reader.options.format.parquet. Drop the duplicate ext fields and the copy, read
the fractions directly, and spell the locals out in full.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mkmkme

mkmkme commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

amd_release and arm_release builds have failed due to issues with infra, not related to the PR directly

@mkmkme

mkmkme commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Stateless tests (amd_debug, parallel)

Failed to start ClickHouse server due to a DNS error:

executeQuery: Code: 499. DB::Exception: Failed to get object info: Code: 198. DB::NetException: Not found address of host: clickhouse-datasets.s3.amazonaws.com. (DNS_ERROR)


Stateless tests (arm_asan_ubsan, targeted)
Stateless tests (arm_asan_ubsan, targeted)

NameError: name 'cidb' is not defined. Unrelated to the PR

@mkmkme

mkmkme commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

PR / RegressionTestsAarch64 / Iceberg (2) / iceberg_2 (pull_request)

Ran locally clickhouse-regression with --only '/iceberg/export partition/no catalog/replicated merge tree/partition compatibility/rejected/rejected﹕ compound field order reversed/*', it succeeded. I consider this to be flaky

@mkmkme

mkmkme commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

@blau-ai

@blau-ai

blau-ai commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

CI triage for #2235 (head 466d6cf)

Verdict: No failure is attributable to this PR. The core praktika CI (builds, unit tests, parallel stateless/integration) is green; the two red praktika jobs are infra flakes, and the red aarch64 RegressionTests + GrypeScan are pre-existing/environment issues unrelated to a Parquet-reader change.

This diff only touches the Parquet v3 reader and its format settings (src/Processors/Formats/Impl/Parquet/*, FormatFactory, FormatSettings). The two new settings default to the same values as before — SettingsChangesHistory records input_format_parquet_prefetch_memory_fraction 0.6→0.6 and input_format_parquet_decode_thread_fraction 0.375→0.375, and the code comment states "Defaults preserve the old hard-coded fractions." So default behaviour is unchanged, which is consistent with the green functional suites.


Praktika functional CI — 2 red, both infra

1. Stateless tests (amd_debug, parallel) — FAIL at "Start ClickHouse Server" → infra flake
The server never came up because the ports were already held by a stale process:

Application: Listen [0.0.0.0]:9181 failed: ... Address already in use ...
Application: Listen [0.0.0.0]:9000 failed: ... Address already in use ...   (8123, 8443, 9009, 9010, ...)

No crash, no assertion, no LOGICAL_ERROR — just port contention (plus benign clickhouse-datasets.s3.amazonaws.com DNS errors). A Parquet-reader change cannot affect port binding at startup.
Safe to re-run.

2. Stateless tests (arm_asan_ubsan, targeted) — ERROR, before any test ran → CI-tooling bug
The "targeted" test selector crashed with a Python NameError in the CI harness, not a ClickHouse failure:

File ".../ci/jobs/scripts/find_tests.py", line 199, in _coverage_cutoff
    result = (cidb.query(...
NameError: name 'cidb' is not defined

ci/jobs/scripts/find_tests.py is not touched by this PR (diff is src/ only). This is a pre-existing bug in the targeted-test coverage lookup and will hit any PR routed through that job.
Pre-existing infra/tooling bug — needs a fix in find_tests.py (cidb is referenced but never bound in _coverage_cutoff), independent of this PR.


Altinity aarch64 RegressionTests — red, pre-existing/environment

cas_aggregate_functions, cas_alter, cas_atomic_insert, cas_engines, cas_lightweight_delete, cas_selects, settings, tiered_storage_cas all show the same retry signature — e.g. 35 retries (7 ok, 2 failed, 26 retried) — across features that have nothing to do with Parquet (ALTER, atomic insert, aggregate functions, tiered storage, settings). Identical failure shapes across unrelated suites is the signature of a shared environment/setup problem in the aarch64 regression runner, not a code regression. settings is essentially green at the scenario level (1700/1708 ok).

Two suites are Parquet-adjacent and worth a confirming re-run, though both fail in small counts amid otherwise-passing runs and the default-preserving change above makes a real regression unlikely:

  • iceberg_2: 1847/1902 scenarios ok, 6 failed (Iceberg data is Parquet-backed).
  • s3_export_part: 89/90 scenarios ok, 1 failed.

Classify as pre-existing/flaky; re-run iceberg_2 and s3_export_part to confirm they clear. If either reproduces on a re-run, ping me with the failing scenario names and I'll dig into whether the v3 scheduler rebalance is involved.

GrypeScan (-alpine image) — pre-existing

1 high/critical vulnerability in the clickhouse-server:...-alpine image. This scans base-image CVEs, not the source diff — unrelated to this PR.


Bottom line: nothing here needs a change to parquet-v3-read-concurrency. Re-run the two praktika jobs and the two Parquet-adjacent regression suites; the find_tests.py cidb NameError is a separate CI-tooling fix. No PR-caused failures found.

@blau-ai · evidence: praktika result_pr.json + clickhouse-server.err.log for 466d6cf

@mkmkme
mkmkme merged commit a07d2ed into antalya-26.6 Aug 25, 2026
368 of 395 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants